package cn.jaye.fabric.application;

import cn.jaye.fabric.application.command.block.InfoBlockCommand;
import cn.jaye.fabric.common.constants.exception.UserExceptionEnum;
import cn.jaye.fabric.common.model.ServiceException;
import cn.jaye.fabric.common.model.SystemSession;
import cn.jaye.fabric.common.util.StringUtil;
import cn.jaye.fabric.domain.model.*;
import cn.jaye.fabric.domain.model.User;
import cn.jaye.fabric.domain.repository.BlockRepository;
import cn.jaye.fabric.domain.repository.TransactionRepository;
import cn.jaye.fabric.domain.service.ChannelSerivce;
import cn.jaye.fabric.domain.service.OrgService;
import cn.jaye.fabric.domain.service.UserService;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.codec.binary.Hex;
import org.hyperledger.fabric.protos.ledger.rwset.kvrwset.KvRwset;
import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.TransactionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import static org.hyperledger.fabric.sdk.BlockInfo.EnvelopeType.TRANSACTION_ENVELOPE;
import static org.junit.Assert.assertEquals;

/**
 * @author Jaye
 * @create 2018-05-21 16:52
 */
@Service
public class BlockApplicationService {

    private Logger logger = LoggerFactory.getLogger(BlockApplicationService.class);
    @Autowired
    private ChannelSerivce channelSerivce;

    @Autowired
    private HFClient hfClient;

    @Autowired
    private UserService userService;

    @Autowired
    private OrgService orgService;
    @Autowired
    private BlockRepository blockRepository;
    @Autowired
    private TransactionRepository transactionRepository;

    private static final String CHARSET_NAME = "UTF-8";

    public MBlockChain listBlockChainInfo(String channelName) throws Exception {
        Channel channel = getChannel(channelName);
        return getBlockChainAllInfo(channel);
    }

    public MBlockChain listBlockChainBaseInfo(String channelName) throws Exception {
        Channel channel = getChannel(channelName);
        return getBlockChainBaseInfo(channel);
    }

    public MBlockChain getBlockChainInfo(String channelName) throws Exception {
        Channel channel = getChannel(channelName);
        MBlockChain mBlockChain = new MBlockChain();
        BlockchainInfo blockchainInfo = channel.queryBlockchainInfo();
        mBlockChain.setHeight(blockchainInfo.getHeight());
        mBlockChain.setCurrentBlockHash(Hex.encodeHexString(blockchainInfo.getCurrentBlockHash()));
        mBlockChain.setPreBlockHash(Hex.encodeHexString(blockchainInfo.getPreviousBlockHash()));
        return mBlockChain;
    }

    private MBlockChain getBlockChainBaseInfo(Channel channel) throws Exception {
        return getBlockChainInfo(channel, "base");
    }

    private MBlockChain getBlockChainAllInfo(Channel channel) throws Exception {
        return getBlockChainInfo(channel, "all");
    }

    private MBlockChain getBlockChainInfo(Channel channel, String type) throws Exception {
        BlockchainInfo blockchainInfo = channel.queryBlockchainInfo();
        MBlockChain mBlockChain = new MBlockChain();
        List<MBlock> mBlocks = new LinkedList<>();
        for (long current = blockchainInfo.getHeight() - 1; current > -1; --current) {
            BlockInfo returnedBlock = channel.queryBlockByNumber(current);
            MBlock mBlock = new MBlock();
            mBlock.setTxCount(returnedBlock.getEnvelopCount());
            mBlock.setBlockNum(returnedBlock.getBlockNumber());
            mBlock.setChannelName(channel.getName());
            mBlock.setDataHash(Hex.encodeHexString(returnedBlock.getDataHash()));
            mBlock.setPreHash(Hex.encodeHexString(returnedBlock.getPreviousHash()));
            mBlock.setCalculateHash(Hex.encodeHexString(SDKUtils.calculateBlockHash(returnedBlock.getBlockNumber(),
                    returnedBlock.getPreviousHash(), returnedBlock.getDataHash())));
            if ("all".equals(type)) {
                mBlock.setMfTransactions(getMFTransaction(returnedBlock));
            }
            mBlocks.add(mBlock);
        }
        mBlockChain.setHeight(blockchainInfo.getHeight());
        mBlockChain.setCurrentBlockHash(Hex.encodeHexString(blockchainInfo.getCurrentBlockHash()));
        mBlockChain.setPreBlockHash(Hex.encodeHexString(blockchainInfo.getPreviousBlockHash()));
        mBlockChain.setMBlocks(mBlocks);
        return mBlockChain;
    }

    public MBlock getBlockByNumber(String channelName, long blockNumber) throws Exception {
        Channel channel = getChannel(channelName);
        BlockInfo returnedBlock = channel.queryBlockByNumber(blockNumber);
        return getBlock(returnedBlock);
    }

    public MBlock getBlockByTxId(String channelName, String txId) throws Exception {
        Channel channel = getChannel(channelName);
        BlockInfo blockInfo = channel.queryBlockByTransactionID(txId);
        return getBlock(blockInfo);
    }

    public MBlock getBlockByHash(String channelName, String hash) throws Exception {
        Channel channel = getChannel(channelName);
        BlockInfo blockInfo = channel.queryBlockByHash(Hex.decodeHex(hash.toCharArray()));
        return getBlock(blockInfo);
    }

    public MFTransaction getTransaction(String channelName, String txId) throws Exception {
        Channel channel = getChannel(channelName);
        BlockInfo blockInfo = channel.queryBlockByTransactionID(txId);
        for (BlockInfo.EnvelopeInfo envelopeInfo : blockInfo.getEnvelopeInfos()) {
            if (envelopeInfo.getTransactionID().equals(txId)) {
                MFTransaction mfTransaction = new MFTransaction(envelopeInfo.getTransactionID(),
                        envelopeInfo.getChannelId(),
                        envelopeInfo.getEpoch(),
                        envelopeInfo.getTimestamp(),
                        convertType(envelopeInfo.getType()),
                        envelopeInfo.isValid(), getMTransactionActionInfo(envelopeInfo));
                return mfTransaction;
            }
        }
        return null;
    }

    private MBlock getBlock(BlockInfo blockInfo) throws Exception {
        MBlock mBlock = new MBlock();
        mBlock.setTxCount(blockInfo.getEnvelopCount());
        mBlock.setBlockNum(blockInfo.getBlockNumber());
        mBlock.setChannelName(blockInfo.getChannelId());
        mBlock.setDataHash(Hex.encodeHexString(blockInfo.getDataHash()));
        mBlock.setPreHash(Hex.encodeHexString(blockInfo.getPreviousHash()));
        mBlock.setCalculateHash(Hex.encodeHexString(SDKUtils.calculateBlockHash(blockInfo.getBlockNumber(),
                blockInfo.getPreviousHash(), blockInfo.getDataHash())));
        mBlock.setMfTransactions(getMFTransaction(blockInfo));
        return mBlock;
    }

    private List<MFTransaction> getMFTransaction(BlockInfo blockInfo) throws Exception {
        List<MFTransaction> mfTransactions = new LinkedList<>();
        for (BlockInfo.EnvelopeInfo envelopeInfo : blockInfo.getEnvelopeInfos()) {
            MFTransaction mfTransaction = new MFTransaction(envelopeInfo.getTransactionID(),
                    envelopeInfo.getChannelId(),
                    envelopeInfo.getEpoch(),
                    envelopeInfo.getTimestamp(),
                    convertType(envelopeInfo.getType()),
                    envelopeInfo.isValid(), getMTransactionActionInfo(envelopeInfo));
            mfTransactions.add(mfTransaction);
        }
        return mfTransactions;
    }

    private List<MTransactionActionInfo> getMTransactionActionInfo(BlockInfo.EnvelopeInfo envelopeInfo) throws Exception {
        List<MTransactionActionInfo> mTransactionActionInfos = new LinkedList<>();
        if (envelopeInfo.getType() == TRANSACTION_ENVELOPE) {
            BlockInfo.TransactionEnvelopeInfo transactionEnvelopeInfo = (BlockInfo.TransactionEnvelopeInfo) envelopeInfo;
            int j = 0;
            for (BlockInfo.TransactionEnvelopeInfo.TransactionActionInfo transactionActionInfo : transactionEnvelopeInfo.getTransactionActionInfos()) {
                ++j;
                List<MEndorserInfo> endorserInfos = new LinkedList<>();
                for (int n = 0; n < transactionActionInfo.getEndorsementsCount(); ++n) {
                    BlockInfo.EndorserInfo endorserInfo = transactionActionInfo.getEndorsementInfo(n);
                    MEndorserInfo mEndorserInfo = new MEndorserInfo(Hex.encodeHexString(endorserInfo.getSignature()),
                            new String(endorserInfo.getEndorser(), CHARSET_NAME));
                    endorserInfos.add(mEndorserInfo);
                }
                List<String> chaincodeInputArgs = new ArrayList<>();
                for (int z = 0; z < transactionActionInfo.getChaincodeInputArgsCount(); ++z) {
                    chaincodeInputArgs.add(new String(transactionActionInfo.getChaincodeInputArgs(z), CHARSET_NAME));
                }
                MTransactionActionInfo mTransactionActionInfo = new MTransactionActionInfo(
                        transactionActionInfo.getResponseStatus(),
                        StringUtil.printableString(new String(transactionActionInfo.getResponseMessageBytes(), CHARSET_NAME)),
                        transactionActionInfo.getEndorsementsCount(),
                        transactionActionInfo.getChaincodeInputArgsCount(),
                        chaincodeInputArgs,
                        endorserInfos,
                        transactionActionInfo.getProposalResponseStatus(),
                        StringUtil.printableString(new String(transactionActionInfo.getProposalResponsePayload(), CHARSET_NAME)),
                        getMTxReadWriteSetInfo(transactionActionInfo.getTxReadWriteSet()));
                mTransactionActionInfos.add(mTransactionActionInfo);
            }
        }
        return mTransactionActionInfos;
    }

    private MTxReadWriteSetInfo getMTxReadWriteSetInfo(TxReadWriteSetInfo txReadWriteSetInfo) throws InvalidProtocolBufferException, UnsupportedEncodingException {
        MTxReadWriteSetInfo mTxReadWriteSetInfo = new MTxReadWriteSetInfo();
        if (null != txReadWriteSetInfo) {
            mTxReadWriteSetInfo.setNsRwsetCount(txReadWriteSetInfo.getNsRwsetCount());
            List<MNsRwsetInfo> mNsRwsetInfos = new LinkedList<>();
            for (TxReadWriteSetInfo.NsRwsetInfo nsRwsetInfo : txReadWriteSetInfo.getNsRwsetInfos()) {
                mNsRwsetInfos.add(getMNsRwsetInfo(nsRwsetInfo));
            }
            mTxReadWriteSetInfo.setMNsRwsetInfos(mNsRwsetInfos);
        }
        return mTxReadWriteSetInfo;
    }

    private MNsRwsetInfo getMNsRwsetInfo(TxReadWriteSetInfo.NsRwsetInfo nsRwsetInfo) throws InvalidProtocolBufferException, UnsupportedEncodingException {
        MNsRwsetInfo mNsRwsetInfo = new MNsRwsetInfo();
        KvRwset.KVRWSet rws = nsRwsetInfo.getRwset();
        MNsRwsetInfo.KVRWSet kvrwSet = new MNsRwsetInfo.KVRWSet();
        List<MNsRwsetInfo.ReadSet> readSetList = new ArrayList<>();
        int rs = -1;
        for (KvRwset.KVRead readList : rws.getReadsList()) {
            rs++;
            MNsRwsetInfo.Version version = new MNsRwsetInfo.Version(readList.getVersion().getBlockNum(), readList.getVersion().getTxNum());
            MNsRwsetInfo.ReadSet readSet = new MNsRwsetInfo.ReadSet(readList.getKey(), version);
            readSetList.add(readSet);
        }
        rs = -1;
        List<MNsRwsetInfo.WriteSet> writeSetList = new ArrayList<>();
        for (KvRwset.KVWrite writeList : rws.getWritesList()) {
            rs++;
            String value = StringUtil.printableString(new String(writeList.getValue().toByteArray(), CHARSET_NAME));
            MNsRwsetInfo.WriteSet writeSet = new MNsRwsetInfo.WriteSet(writeList.getKey(), value);
            writeSetList.add(writeSet);
        }
        kvrwSet.setReadSetList(readSetList);
        kvrwSet.setWriteSetList(writeSetList);

        mNsRwsetInfo.setKvrwSet(kvrwSet);
        mNsRwsetInfo.setNamespace(nsRwsetInfo.getNaamespace());
        return mNsRwsetInfo;
    }

    private String convertType(BlockInfo.EnvelopeType envelopeType) {
        if (envelopeType == TRANSACTION_ENVELOPE) {
            return "TRANSACTION_ENVELOPE";
        }
        return "ENVELOPE";
    }

    private Channel getChannel(String channelName) throws TransactionException, InvalidArgumentException {
        User user = SystemSession.getUser();
        checkUser(user);
        return channelSerivce.getChannelByRelPeer(channelName, orgService.getOrg(user.getOrgId()));
    }

    public Long blockNum(InfoBlockCommand infoBlockCommand) throws Exception {
        User user = SystemSession.getUser();
        checkUser(user);
        Channel channel = channelSerivce.getChannelByRelPeer(infoBlockCommand.getChannelName(), orgService.getOrg(user.getOrgId()));
//        BlockchainInfo blockchainInfo = channel.queryBlockchainInfo();
        BlockchainInfo channelInfo = channel.queryBlockchainInfo();
//        channelInfo.get
//        BlockInfo channel.queryBlockByTransactionID("2");
//        channel.q
        for (long current = channelInfo.getHeight() - 1; current > -1; --current) {
            BlockInfo returnedBlock = channel.queryBlockByNumber(current);
//
            final long blockNumber = returnedBlock.getBlockNumber();
            System.out.println("---------------------------");
            logger.error("current block number {} has data hash: {}", blockNumber, Hex.encodeHexString(returnedBlock.getDataHash()));
            logger.error("current block number {} has previous hash id: {}", blockNumber, Hex.encodeHexString(returnedBlock.getPreviousHash()));
            logger.error("current block number {} has calculated block hash is {}", blockNumber, Hex.encodeHexString(SDKUtils.calculateBlockHash(blockNumber, returnedBlock.getPreviousHash(), returnedBlock.getDataHash())));
            System.out.println("---------------------------");
            final int envelopCount = returnedBlock.getEnvelopCount();
//            assertEquals(1, envelopCount);
            int i = 0;
            MBlock mBlock = new MBlock();
            mBlock.setBlockNum(blockNumber);
            mBlock.setDataHash(Hex.encodeHexString(returnedBlock.getDataHash()));
            mBlock.setPreHash(Hex.encodeHexString(returnedBlock.getPreviousHash()));
            mBlock.setTxCount(envelopCount);
            mBlock.setChannelName(channel.getName());
            blockRepository.insert(mBlock);
            for (BlockInfo.EnvelopeInfo envelopeInfo : returnedBlock.getEnvelopeInfos()) {
                ++i;
                logger.error("  Transaction number {} has transaction id: {}", i, envelopeInfo.getTransactionID());
                final String channelId = envelopeInfo.getChannelId();
                logger.error("  Transaction number {} has channel id: {}", i, channelId);
                logger.error("  Transaction number {} has epoch: {}", i, envelopeInfo.getEpoch());
                logger.error("  Transaction number {} has transaction timestamp: {}", i, envelopeInfo.getTimestamp());
                logger.error("  Transaction number {} has type id: {}", i, "" + envelopeInfo.getType());


                MTransaction mTransaction = new MTransaction();
                mTransaction.setTxId(envelopeInfo.getTransactionID());
                mTransaction.setChannelName(channelId);
                mTransaction.setBlockNum(blockNumber);
                mTransaction.setCreateTime(envelopeInfo.getTimestamp());
                transactionRepository.insert(mTransaction);
                if (envelopeInfo.getType() == TRANSACTION_ENVELOPE) {
                    BlockInfo.TransactionEnvelopeInfo transactionEnvelopeInfo = (BlockInfo.TransactionEnvelopeInfo) envelopeInfo;
                    logger.error("  Transaction number {} has {} actions", i, transactionEnvelopeInfo.getTransactionActionInfoCount());
                    assertEquals(1, transactionEnvelopeInfo.getTransactionActionInfoCount());
                    logger.error("  Transaction number {} isValid {}", i, transactionEnvelopeInfo.isValid());
//                    assertEquals(transactionEnvelopeInfo.isValid(), true);
                    logger.error("  Transaction number {} validation code {}", i, transactionEnvelopeInfo.getValidationCode());
//                    assertEquals(0, transactionEnvelopeInfo.getValidationCode());
                    int j = 0;
                    for (BlockInfo.TransactionEnvelopeInfo.TransactionActionInfo transactionActionInfo : transactionEnvelopeInfo.getTransactionActionInfos()) {
                        ++j;
                        logger.error("   Transaction action {} has response status {}", j, transactionActionInfo.getResponseStatus());
                        assertEquals(200, transactionActionInfo.getResponseStatus());
                        logger.error("   Transaction action {} has response message bytes as string: {}", j,
                                printableString(new String(transactionActionInfo.getResponseMessageBytes(), "UTF-8")));
                        logger.error("   Transaction action {} has {} endorsements", j, transactionActionInfo.getEndorsementsCount());
//                        assertEquals(2, transactionActionInfo.getEndorsementsCount());

                        for (int n = 0; n < transactionActionInfo.getEndorsementsCount(); ++n) {
                            BlockInfo.EndorserInfo endorserInfo = transactionActionInfo.getEndorsementInfo(n);
                            logger.error("Endorser {} signature: {}", n, Hex.encodeHexString(endorserInfo.getSignature()));
                            logger.error("Endorser {} endorser: {}", n, new String(endorserInfo.getEndorser(), "UTF-8"));
                        }
                        logger.error("   Transaction action {} has {} chaincode input arguments", j, transactionActionInfo.getChaincodeInputArgsCount());
                        for (int z = 0; z < transactionActionInfo.getChaincodeInputArgsCount(); ++z) {
                            logger.error("     Transaction action {} has chaincode input argument {} is: {}", j, z,
                                    printableString(new String(transactionActionInfo.getChaincodeInputArgs(z), "UTF-8")));
                        }
                        logger.error("   Transaction action {} proposal response status: {}", j,
                                transactionActionInfo.getProposalResponseStatus());
                        logger.error("   Transaction action {} proposal response payload: {}", j,
                                printableString(new String(transactionActionInfo.getProposalResponsePayload())));

                        TxReadWriteSetInfo rwsetInfo = transactionActionInfo.getTxReadWriteSet();
                        if (null != rwsetInfo) {
                            logger.error("   Transaction action {} has {} name space read write sets", j, rwsetInfo.getNsRwsetCount());

                            for (TxReadWriteSetInfo.NsRwsetInfo nsRwsetInfo : rwsetInfo.getNsRwsetInfos()) {
                                final String namespace = nsRwsetInfo.getNaamespace();
                                KvRwset.KVRWSet rws = nsRwsetInfo.getRwset();

                                int rs = -1;
                                for (KvRwset.KVRead readList : rws.getReadsList()) {
                                    rs++;

                                    logger.error("     Namespace {} read set {} key {}  version [{}:{}]", namespace, rs, readList.getKey(),
                                            readList.getVersion().getBlockNum(), readList.getVersion().getTxNum());

                                }

                                rs = -1;
                                for (KvRwset.KVWrite writeList : rws.getWritesList()) {
                                    rs++;
                                    String valAsString = printableString(new String(writeList.getValue().toByteArray(), "UTF-8"));

                                    logger.error("     Namespace {} write set {} key {} has value '{}' ", namespace, rs,
                                            writeList.getKey(),
                                            valAsString);

                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    static String printableString(final String string) {
        int maxLogStringLength = 64;
        if (string == null || string.length() == 0) {
            return string;
        }

        String ret = string.replaceAll("[^\\p{Print}]", "?");

        ret = ret.substring(0, Math.min(ret.length(), maxLogStringLength)) + (ret.length() > maxLogStringLength ? "..." : "");

        return ret;
    }

    private void checkUser(User user) {
        try {
            hfClient.setUserContext(userService.getUser(user));
        } catch (InvalidArgumentException e) {
            e.printStackTrace();
            throw new ServiceException(UserExceptionEnum.USER_ERROR);
        }
    }
}
